import java.util.*;

/**
 * Implementation of SyntaxAnalyzer interface by Josh Dehlinger (2013)
 * checks grammar rules and changes mark down language to html5
 * @author Colin Murray
 */
public class MySyntaxAnalyzer implements SyntaxAnalyzer {
	
	MyLexicalAnalyzer lex;
	int flen;
	ArrayList<String> html5 = new ArrayList<String>();
	ArrayList<String> vnames = new ArrayList<String>();
	ArrayList<String> vvals = new ArrayList<String>();
	
	/**
	 * constructor for MySyntaxAnalyzer object
	 * @param a the input file as a string
	 * @param b the lexical analyzer instance
	 * @throws CompilerException
	 */
	public MySyntaxAnalyzer( String a, MyLexicalAnalyzer b ) throws CompilerException {
		lex = b;
		flen = a.length();
		document();
	}
	
	public void document() throws CompilerException {
		lex.getNextToken();
		if( lex.getTok().equalsIgnoreCase( "#start document" ) ) {
			html5.add( "<html>" );
			lex.getNextToken();
			space();
			int vcnt = define();
			space();
			head();
			body();
			if ( lex.getTok().equalsIgnoreCase( "#end document" ) ) {
				html5.add( "</html>" );
				if ( lex.getPos() < flen ) {
					String tmp = "";
					while ( lex.getPos() < flen ) {
						lex.getNextToken();
						tmp += lex.getTok();
					}
					if( tmp.trim().length() > 0 ) {
						throw new CompilerException( "Syntax error! stuff after #end document" );
					}
				}
			}
			else {
				if ( lex.getPos() < flen ) {
					throw new CompilerException("Syntax error! unexpected token" );
				}
				else {
					throw new CompilerException("Syntax error! missing #end document" );
				}
			}
			if( vcnt > 0) {
				vnames.remove(vnames.size() - vcnt);
				vvals.remove(vvals.size() - vcnt);
			}
		}
		else {
			throw new CompilerException("Syntax error! expected #start document" );
		}
	}

	public void head() throws CompilerException {
		if( lex.getTok().equalsIgnoreCase( "#start head" ) ) {
			html5.add( "<head>" );
			lex.getNextToken();
			space();
			title();
			space();
			if ( lex.getTok().equalsIgnoreCase( "#end head" ) ) {
				html5.add( "</head>" );
				lex.getNextToken();
			}
			else {
				throw new CompilerException("Syntax error! missing #end head" );
			}
		}
	}

	public void title() throws CompilerException {
		if( lex.getTok().equalsIgnoreCase( "#start title" ) ) {
			html5.add( "<title>" );
			lex.getNextToken();
			text();
			if ( lex.getTok().equalsIgnoreCase( "#end title" ) ) {
				html5.add( "</title>" );
				lex.getNextToken();
			}
			else {
				throw new CompilerException("Syntax error! missing #end title" );
			}
		}
	}
	
	/**
	 * This method implements the BNF grammar rule for the body of an html5 document.
	 * @throws CompilerException
	 */
	public void body() throws CompilerException {
		int c = 0;
		if ( ( !lex.getTok().isEmpty() ) && ( !lex.getTok().equalsIgnoreCase( "#end document" ) ) ) {
			c += space();
			c += innerText();
			c += paragraph();
			c += bold();
			c += italics();
			c += list();
		}
		if (c > 0) {
			body();
		}
	}
	
	public int paragraph() throws CompilerException {
		int c = 0;
		if( lex.getTok().equalsIgnoreCase( "#start paragraph" ) ) {
			html5.add( "<p>" );
			lex.getNextToken();
			c++;
			space();
			int vcnt = define();
			innerParagraph();
			space();
			if ( lex.getTok().equalsIgnoreCase( "#end paragraph" ) ) {
				html5.add( "</p>" );
				lex.getNextToken();
			}
			else {
				throw new CompilerException("Syntax error! missing #end paragraph" );
			}
			if( vcnt > 0) {
				vnames.remove(vnames.size() - vcnt);
				vvals.remove(vvals.size() - vcnt);
			}
		}
		return c;
	}

	/**
	 * This method implements the BNF grammar rule for the content of an html5 paragraph.
	 * @throws CompilerException
	 */
	public void innerParagraph() throws CompilerException {
		int c = 0;
		if ( !lex.getTok().equalsIgnoreCase( "#end paragraph" ) ) {
			c += space();
			c += innerText();
			c += bold();
			c += italics();
			c += list();
		}
		if ( c > 0 ) {
			innerParagraph();
		}
	}

	/**
	 * This method implements the BNF grammar rule for text in html5.
	 * @throws CompilerException
	 */
	public int innerText() throws CompilerException {
		int c = 0;
		if ( ( !lex.getTok().isEmpty() ) && ( lex.getTok().charAt( 0 ) != '#' ) 
				|| ( lex.getTok().equalsIgnoreCase( "#start use" ) ) 
				|| ( lex.getTok().equalsIgnoreCase( "#newline" ) ) ) {
			c += text();
			c += use();
			c += newline();
		}
		if ( ( c > 0) ){
			return c + innerText();
		}
		else {
			return c;
		}
	}
	
	public int define() throws CompilerException {
		space();
		if ( lex.getTok().equalsIgnoreCase( "#start define" ) ) {
			lex.getNextToken();
			space();
			if( lex.getTok().equalsIgnoreCase( "#name" ) ) {
				lex.getNextToken();
				space();
				vnames.add( lex.getTok() );
			}
			else {
				throw new CompilerException("Syntax error! missing #name" );
			}
			space();
			lex.getNextToken();
			if ( lex.getTok().equalsIgnoreCase( "#value" ) ) {
				lex.getNextToken();
				space();
				vvals.add( lex.getTok() );
			}
			else {
				throw new CompilerException("Syntax error! missing #value" );
			}
			space();
			lex.getNextToken();
			if ( lex.getTok().equalsIgnoreCase( "#end define" ) ) {
				lex.getNextToken();
			}
			else {
				throw new CompilerException("Syntax error! missing #end define" );
			}
			space();
			if ( lex.getTok().equalsIgnoreCase( "#start define" ) ) {
				return ( 1 + define() );
			}
			else {
				return 1;
			}
		}
		return 0;
	}

	public int use() throws CompilerException {
		int c = 0;
		if( lex.getTok().equalsIgnoreCase( "#start use" ) ) {
			lex.getNextToken();
			c++;
			int i = vnames.lastIndexOf(lex.getTok());
			if ( i < 0 ) {
				throw new CompilerException("Semantic error! use of undefined variable" );
			}
			else {
				html5.add( vvals.get( i ) );
			}
			space();
			lex.getNextToken();
			if ( lex.getTok().equalsIgnoreCase( "#end use" ) ) {
				lex.getNextToken();
			}
			else {
				throw new CompilerException("Syntax error! missing #end use" );
			}
		}
		return c;
	}

	public int bold() throws CompilerException {
		int c = 0;
		if( lex.getTok().equalsIgnoreCase( "#start bold" ) ) {
			html5.add( "<b>" );
			lex.getNextToken();
			c++;
			space();
			int vcnt = define();
			innerText();
			if ( lex.getTok().equalsIgnoreCase( "#end bold" ) ) {
				html5.add( "</b>" );
				lex.getNextToken();
			}
			else {
				throw new CompilerException("Syntax error! missing #end bold" );
			}
			if( vcnt > 0) {
				vnames.remove(vnames.size() - vcnt);
				vvals.remove(vvals.size() - vcnt);
			}
		}
		return c;
	}

	public int italics() throws CompilerException {
		int c = 0;
		if( lex.getTok().equalsIgnoreCase( "#start italics" ) ) {
			html5.add( "<i>" );
			lex.getNextToken();
			c++;
			space();
			int vcnt = define();
			innerText();
			if ( lex.getTok().equalsIgnoreCase( "#end italics" ) ) {
				html5.add( "</i>" );
				lex.getNextToken();
			}
			else {
				throw new CompilerException("Syntax error! missing #end italics" );
			}
			if( vcnt > 0) {
				vnames.remove(vnames.size() - vcnt);
				vvals.remove(vvals.size() - vcnt);
			}
		}
		return c;
	}

	public int list() throws CompilerException {
		int c = 0;
		if( lex.getTok().equalsIgnoreCase( "#start list" ) ) {
			html5.add( "<ul>" );
			lex.getNextToken();
			c++;
			space();
			if( lex.getTok().equalsIgnoreCase( "#start item" ) ) {
				html5.add( "<li>" );
				lex.getNextToken();
				space();
				int vcnt = define();
				space();
				innerList();
				space();
				if ( lex.getTok().equalsIgnoreCase( "#end item" ) ) {
					html5.add( "</li>" );
					lex.getNextToken();
				}
				else {
					throw new CompilerException("Syntax error! missing #end item" );
				}
				if( vcnt > 0) {
					vnames.remove(vnames.size() - vcnt);
					vvals.remove(vvals.size() - vcnt);
				}
			}
			space();
			item();
			space();
			if ( lex.getTok().equalsIgnoreCase( "#end list" ) ) {
				html5.add( "</ul>" );
				lex.getNextToken();
			}
			else {
				throw new CompilerException("Syntax error! missing #end list" );
			}
		}
		return c;
	}

	public void item() throws CompilerException {
		space();
		if( lex.getTok().equalsIgnoreCase( "#start item" ) ) {
				html5.add( "<li>" );
				lex.getNextToken();
				space();
				int vcnt = define();
				space();
				innerList();
				space();
				if ( lex.getTok().equalsIgnoreCase( "#end item" ) ) {
					html5.add( "</li>" );
					lex.getNextToken();
				}
				else {
					throw new CompilerException("Syntax error! missing #end item" );
				}
				if( vcnt > 0) {
					vnames.remove(vnames.size() - vcnt);
					vvals.remove(vvals.size() - vcnt);
				}
		}
		space();
		if( lex.getTok().equalsIgnoreCase( "#start item" ) ) {
			item();
		}
	}


	/**
	 * This method implements the BNF grammar rule for the content of a list.
	 * @throws CompilerException
	 */
	public void innerList() throws CompilerException {
		int c = 0;
		if( !lex.getTok().equalsIgnoreCase( "#end item" ) ) {
			c += space();
			c += bold();
			c += italics();
			c += list();
			c += innerText();
		}
		if ( c > 0) {
			innerList();
		}
	}

	public void audio() throws CompilerException {
		if( lex.getTok().equalsIgnoreCase( "#start audio" ) ) {
			html5.add( "<audio controls> <source src=\"" );
			lex.getNextToken();
			space();
			innerText();
			if ( lex.getTok().equalsIgnoreCase( "#end audio" ) ) {
				html5.add( "\"> </audio>" );
				lex.getNextToken();
			}
			else {
				throw new CompilerException("Syntax error! missing #end audio" );
			}
		}
	}

	public void video() throws CompilerException {
		if( lex.getTok().equalsIgnoreCase( "#start video" ) ) {
			html5.add( "<iframe src=\"" );
			lex.getNextToken();
			space();
			innerText();
			if ( lex.getTok().equalsIgnoreCase( "#end video" ) ) {
				html5.add( "\"/>" );
				lex.getNextToken();
			}
			else {
				throw new CompilerException("Syntax error! missing #end video" );
			}
		}
	}
	

	/**
	 * This method implements the BNF grammar rule for the newline annotation.
	 * @return > 0 if current token matches a valid tag
	 * @throws CompilerException
	 */
	public int newline() throws CompilerException {
		int c = 0;
		if( lex.getTok().equalsIgnoreCase( "#newline" ) ) {
			html5.add( "<br>" );
			lex.getNextToken();
			c++;
		}
		return c;
	}
	
	/**
	 * This method implements the BNF grammar rule for whitespace.
	 * @return > 0 if current token matches a valid tag
	 * @throws CompilerException
	 */
	public int space() throws CompilerException {
		int c = 0;
		if( ( !lex.getTok().isEmpty() ) && ( lex.isSpace(lex.getTok()) ) ) {
			html5.add( lex.getTok() );
			lex.getNextToken();
			c++;
		}
		return c;
	}
	
	/**
	 * This method implements the BNF grammar rule for text.
	 * @return > 0 if current token matches a valid tag
	 * @throws CompilerException
	 */
	public int text() throws CompilerException {
		int c = 0;
		if( ( !lex.getTok().isEmpty() ) && ( lex.getTok().charAt( 0 ) != '#' ) ) {
			html5.add( lex.getTok() );
			lex.getNextToken();
			c++;
		}
		return c;
	}
	
	/**
	 * This method returns the master list of html5
	 */
	public ArrayList<String> getHtml5() {
		return html5;
	}
}
